package tech.yixiyun.framework.kuafu.db.sql;

import com.fasterxml.jackson.databind.JavaType;
import tech.yixiyun.framework.kuafu.bean.BeanContext;
import tech.yixiyun.framework.kuafu.db.DbException;
import tech.yixiyun.framework.kuafu.db.datasource.DbType;
import tech.yixiyun.framework.kuafu.db.transaction.TransactionEnhancer;
import tech.yixiyun.framework.kuafu.domain.BaseDomain;
import tech.yixiyun.framework.kuafu.enhance.annotation.Enhance;

import java.io.Serializable;
import java.util.HashMap;
import java.util.List;

/**
 * 语句执行器。
 */
@Enhance(TransactionEnhancer.class)
public abstract class SqlExecutor {
    /**
     * 注册的
     */
    private static final HashMap<DbType, Class<? extends SqlExecutor>> EXECUTOR_MAP = new HashMap<>();

    /**
     * 注册sql执行器
     *
     * @param dbType
     * @param executorClass
     */
    public static void registerExecutor(DbType dbType, Class<? extends SqlExecutor> executorClass) {
        synchronized (EXECUTOR_MAP) {
            EXECUTOR_MAP.put(dbType, executorClass);
        }
    }

    /**
     * 获取某个数据库类型的sql执行器
     *
     * @param dbType
     * @return
     */
    public static SqlExecutor getSqlExecutor(DbType dbType) {
        Class<? extends SqlExecutor> clazz = EXECUTOR_MAP.get(dbType);
        if (clazz == null) {
            throw new DbException("未注册" + dbType + "的SqlExecutor");
        }
        return BeanContext.getBean(clazz);
    }


    /**
     * 执行一条语句，这个语句可以是任意语句。
     *
     * @param dataSourceName 语句执行使用的数据源
     * @param statement      语句，如果传入了args，就是用preparedstatement查询，没有就用statement
     * @param args           如果是preparedstatement,这个就是要传入的值，
     * @return int 受影响的行数，如果执行的是一个select语句，返回-1
     */
    public abstract int execute(String dataSourceName, String statement, Serializable[] args);

    /**
     * 执行一条语句，这个语句可以是任意语句
     *
     * @param sql
     * @return 受影响的行数
     */
    public abstract int execute(Sql sql);

    /**
     * 根据Domain类进行建表，如果表已存在，就不会执行
     *
     * @param dataSourceName
     * @param domainClass
     * @param args           如果是分表，需要传入的表名参数
     */
    public abstract void createTable(String dataSourceName, Class<? extends BaseDomain> domainClass, Serializable... args);


    /**
     * 分表修改表结构<br/>
     * 注意该方法只能为表结构添加新的字段，已存在字段不会做处理，因为可能涉及数据转换，需要你根据情况手动处理。
     *
     * @param dataSourceName
     * @param domainClass
     * @param args
     */
    public abstract void alterTable(String dataSourceName, Class<? extends BaseDomain> domainClass, Serializable... args);


    /**
     * 检查表的某个字段是否 存在
     *
     * @param dataSourceName 数据源名
     * @param tableName      表名
     * @param columnName     列名
     * @return
     */
    public abstract boolean columnExist(String dataSourceName, String tableName, String columnName);

    /**
     * 检测某个数据源有没有某个表
     *
     * @param dataSourceName
     * @param tableName
     * @return
     */
    public abstract boolean tableExist(String dataSourceName, String tableName);

    /**
     * 根据一个实例插入一条记录，如果包含自增字段，语句执行完后会自动更新到实例中
     *
     * @param dataSourceName
     * @param tableName
     * @param instance
     * @return 如果包含自增字段，那么就返回自增的id，否则返回null
     */
    public abstract Object insertOne(String dataSourceName, String tableName, BaseDomain instance);
    /**
     * 根据一个实例插入一条记录，前提是指定列 在同表中不存在 和本次要插入的值一致的记录。
     * 如果包含自增字段，语句执行完后会自动更新到实例中
     * @param dataSourceName
     * @param tableName
     * @param instance
     * @param checkCols 要检查的列名
     * @return 如果包含自增字段，那么就返回自增的id，否则返回null
     */
    public abstract Object insertOneIfNotExist(String dataSourceName, String tableName, BaseDomain instance, String[] checkCols);


    /**
     * 向主数据源中，根据Sql查询语句，向指定表、列插入数据<br/>
     * insert into ... (...,...) select ..... from ... where ...
     *
     * @param tableName
     * @param cols
     * @param querySql  数据来源查询语句
     * @return 返回插入的数据条数
     */
    public abstract int insert(String dataSourceName, String tableName, String[] cols, Sql querySql);

    /**
     * 指定数据源、表、列、值进行插入数据
     *
     * @param dataSourceName
     * @param tableName
     * @param cols
     * @param values
     * @return
     */
    public abstract Long insert(String dataSourceName, String tableName, String[] cols, Serializable[] values);
    /**
     * 指定数据源、表、列、值进行插入数据，前提是指定条件的记录不存在
     *
     * @param dataSourceName
     * @param tableName
     * @param insertColAndValues 要插入的列名和值 数组，按照 列名，值，列名，值的顺序传入
     * @param checkColAndValues 要检测的列名和值 数组，按照 列名，值，列名，值的顺序传入
     * @return
     */
    public abstract Long insertIfNotExist(String dataSourceName, String tableName, Serializable[] insertColAndValues, Serializable[] checkColAndValues );
    /**
     * 批量插入
     * @param dataSourceName
     * @param tableName
     * @param instances
     * @param batchCount 分批插入，每次批量插入的数量。
     */
    public abstract List<? extends BaseDomain> insertBatch(String dataSourceName, String tableName, List<? extends BaseDomain> instances, int batchCount);

    /**
     * 执行一条查询语句，获取第一行结果，并自动转换为resultClass类的对象
     * @param dataSourceName 使用的数据源名称
     * @param statement 要执行的查询语句
     * @param args 执行语句需要传入的参数
     * @param resultClass 将结果转换为的类
     * @param <T>
     * @return
     */
    public abstract <T> T getOne(String dataSourceName, String statement, Serializable[] args, Class<T> resultClass);


    /**
     * 执行一条查询语句，获取第一行某一列的数据，并自动转换为resultClass类的对象
     * @param dataSourceName 使用的数据源名称
     * @param statement 要执行的查询语句
     * @param args 执行语句需要传入的参数
     * @param columnClass 将结果转换为的类,支持基本数据类型及包装类和他们的数组、String和String数组、Date、Time、byte[]
     * @param <T>
     * @return
     */
    public abstract <T> T getColumn(String dataSourceName, String statement, Serializable[] args, Class<T> columnClass);
    /**
     * 执行一条查询语句，获取第一行某一列的数据，并自动转换为columnType类的对象。适用于转化json格式数据
     * @param dataSourceName 使用的数据源名称
     * @param statement 要执行的查询语句
     * @param args 执行语句需要传入的参数
     * @param columnType 将结果转换为的类,jackson提供的反序列化类
     * @param <T>
     * @return
     */
    public abstract <T> T getColumn(String dataSourceName, String statement, Serializable[] args, JavaType columnType);

    /**
     * sum统计，sql可以只拼接条件语句
     * @param sql
     * @return
     */
    public abstract int getCount(Sql sql);

    /**
     * sum统计，sql可以只拼接条件语句
     * @param columnName 要统计的列名
     * @param sql 条件语句
     * @param resultClass sum结果转换的类型
     * @return
     */
    public abstract  <T> T getSum(String columnName, Sql sql, Class<T> resultClass);

    /**
     * 执行查询语句，获取数据，并自动转换为resultClass类的对象集合
     * @param dataSourceName 使用的数据源名称
     * @param statement 要执行的查询语句
     * @param args 执行语句需要传入的参数
     * @param resultClass 将结果转换为的类,支持Domain类、POJO类、Kv、Map、HashMap、LinkedHashMap
     * @param <T>
     * @return
     */
    public abstract <T> List<T> getList(String dataSourceName, String statement, Serializable[] args, Class<T> resultClass);

    /**
     * 执行更新语句
     * @param dataSourceName
     * @param statement
     * @param args 执行语句需要传入的参数
     * @return 影响的数据行数
     */
    public abstract int update(String dataSourceName, String statement, Serializable[] args);

    /**
     * 根据实例更新数据库中信息，需要实例必须有主键且主键必须有值
     * @param dataSourceName
     * @param tableName
     * @param instance
     * @return
     */
    public abstract int updateOne(String dataSourceName, String tableName, BaseDomain instance);

    /**
     * 批量更新
     * @param dataSourceName
     * @param tableName
     * @param instances
     * @param batchCount 每次批量执行多少条
     * @return
     */
    public abstract int updateBatch(String dataSourceName, String tableName, List<? extends BaseDomain> instances, int batchCount);

    /**
     * 主键有值就根据主键值找到对应记录更新其数据，
     * 没有主键或者主键值为null，就插入
     * @param dataSourceName
     * @param tableName
     * @param instance
     * @return
     */
    public abstract BaseDomain saveOne(String dataSourceName, String tableName, BaseDomain instance);
    /**
     * 批量保存数据，注意该方法效率并不高，不是批量执行语句，因为需要对每一个实例进行判断要insert还是update。
     * 所以尽量不要用该方法。
     * @param dataSourceName
     * @param tableName
     * @param instances
     * @param batchCount 每次批量执行多少条
     * @return
     */
    public abstract List<? extends BaseDomain> saveBatch(String dataSourceName, String tableName, List<? extends BaseDomain> instances, int batchCount);

    /**
     * 执行删除语句
     * @param sql
     * @return
     */
    public abstract int del(Sql sql);

    /**
     * 根据实例进行删除，实例必须有主键且主键必须有值
     * @param dataSourceName
     * @param tableName
     * @param instance
     * @return
     */
    public abstract int delOne(String dataSourceName, String tableName, BaseDomain instance);
}
